﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Windows;
using BorgExt;
using BorgExt.Startup;

namespace Borg.Desktop
{
    internal delegate void StartupHandler();

    public class Initializer : InitializerBase
    {
        private readonly BorgExt.LogEventArgs[] _initialLogs;

        private Initializer(RuntimeConfig config, BorgExt.LogEventArgs[] initialLogs, string logfile, bool noLogo)
            : base(config)
        {
            _initialLogs = initialLogs;
            SetupLogger(logfile, noLogo);
        }

        private static void BootstrapDiscovererActivated(BorgExt.ExtensionEventArgs args)
        {
            ((BootstrapDiscoverer)args.Extension.Object).BootstrapPaths = BootstrapPaths;
        }

        private static void BootstrapDiscovererCreated(BorgExt.ExtensionEventArgs args)
        {
            args.Extension.Activated += BootstrapDiscovererActivated;
        }

        private static AppDomain CreateAppDomain(IEnumerable<string> lookupDirectories)
        {
            var appDomainSetup = new AppDomainSetup();
            var builder = new StringBuilder();

            foreach (var dir in lookupDirectories)
            {
                builder.Append(dir);
                builder.Append(';');
            }

            appDomainSetup.PrivateBinPath =
                appDomainSetup.ShadowCopyDirectories = builder.ToString();

            appDomainSetup.ShadowCopyFiles = "true";
            appDomainSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

            return AppDomain.CreateDomain(
                "default.default",
                AppDomain.CurrentDomain.Evidence,
                appDomainSetup);
        }

        private static string[] GetAssemblyDirectories(IEnumerable<string> assemblyDirectories,
            ISet<string> lookupDirectories, ICollection<BorgExt.LogEventArgs> initialLogs)
        {
            var list = new List<string>();

            foreach (var dir in assemblyDirectories)
            {
                if (Directory.Exists(dir))
                {
                    var fullPath = Path.GetFullPath(dir);

                    if (!list.Contains(fullPath))
                    {
                        list.Add(fullPath);
                        lookupDirectories.Add(fullPath);

                        foreach (var pth in Directory.GetDirectories(fullPath, "*", SearchOption.AllDirectories))
                            lookupDirectories.Add(pth);
                    }
                }
                else
                    initialLogs.Add(
                        new BorgExt.LogEventArgs(
                            LogType.Warning,
                            string.Format("Discovery directory not found: '{0}'", dir)));
            }

            return list.ToArray();
        }

        private static string[] GetBootstrapPaths(string[] bootstrapPaths,
            ISet<string> lookupDirectories, ICollection<BorgExt.LogEventArgs> initialLogs)
        {
            if (bootstrapPaths == null || bootstrapPaths.Length == 0)
            {
                var fullPath = Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory);

                lookupDirectories.Add(fullPath);
                return new[] { fullPath };
            }

            var list = new List<string>();
            var flag = false;

            foreach (var bootPath in bootstrapPaths)
            {
                string addedPath;

                if (File.Exists(bootPath))
                {
                    addedPath = Path.GetFullPath(bootPath);

                    if (!list.Contains(addedPath))
                        list.Add(addedPath);

                    lookupDirectories.Add(Path.GetDirectoryName(addedPath));
                }
                else if (Directory.Exists(bootPath))
                {
                    addedPath = Path.GetFullPath(bootPath);

                    foreach (var dllPath in Directory.GetFiles(addedPath, "*.dll").
                        Where(dllPath => !list.Contains(dllPath)))
                        list.Add(dllPath);

                    lookupDirectories.Add(addedPath);
                }
                else
                {
                    flag = true;
                    initialLogs.Add(
                        new BorgExt.LogEventArgs(
                            LogType.Error,
                            string.Format("Bootstrap file or directory not found: '{0}'", bootPath)));
                }
            }

            return flag
                ? null
                : list.ToArray();
        }

        private static void SetupAppDomain(IEnumerable<string> lookupPaths)
        {
            foreach (var pth in lookupPaths)
                AppDomain.CurrentDomain.AppendPrivatePath(pth);

            AppDomain.CurrentDomain.SetShadowCopyFiles();
        }

        private void SetupLogger(string logfile, bool noLogo)
        {
            if (!noLogo)
                ConsoleLogger.PrintLogo();

            runtime.Logger.Log += ConsoleLogger.OnLog;

            if (string.IsNullOrEmpty(logfile))
                return;

            var logger = new FileLogger(logfile, noLogo);

            runtime.Logger.Log += logger.OnLog;
            runtime.ShutdownFinished += logger.OnShutdownFinished;
        }

        private void Start()
        {
            var contracts = new List<Contract>();
            var plugins = new List<Plugin>();

            Analyze(typeof(IApplication).Assembly, contracts, plugins);
            Analyze(typeof(Startup).Assembly, contracts, plugins);

            var startup = plugins[0].ExtensionTypes["Startup"];
            var bootstrap = plugins[0].ExtensionTypes["BootstrapDiscoverer"];

            bootstrap.Created += BootstrapDiscovererCreated;
            Start(contracts, plugins, startup, _initialLogs);
        }

        private void Start(object sender, StartupEventArgs e)
        {
            lock (this)
            {
                Start();
                Monitor.PulseAll(this);
            }
        }

        public static BorgExt.Runtime Start(string[] bootstrapPaths, string[] assemblyDirectories,
            KeyValuePair<string, string>[] arguments, BorgExt.VerbosityLevel verbosityLevel,
            bool noLogo, string logfile, bool trace, bool createAppDomain)
        {
            Initializer initializer;

            var lookupDirectories = new HashSet<string>();
            var initialLogs = new List<BorgExt.LogEventArgs>();

            BootstrapPaths = GetBootstrapPaths(bootstrapPaths, lookupDirectories, initialLogs);
            assemblyDirectories = GetAssemblyDirectories(assemblyDirectories, lookupDirectories, initialLogs);

            var config = new RuntimeConfig
            {
                AssemblyDirectories = assemblyDirectories,
                Arguments = arguments,
                VerbosityLevel = verbosityLevel,
                TrackCalls = trace
            };

            if (createAppDomain)
                initializer =
                    (Initializer)CreateAppDomain(lookupDirectories).CreateInstance(
                        typeof(Initializer).Assembly.FullName,
                        typeof(Initializer).FullName,
                        false,
                        BindingFlags.Default,
                        null,
                        new object[]
                            {
                                config,
                                initialLogs.ToArray(),
                                logfile,
                                noLogo
                            },
                        null,
                        null).Unwrap();
            else
            {
                SetupAppDomain(lookupDirectories);
                initializer = new Initializer(config, initialLogs.ToArray(), logfile, noLogo);
            }

            if (Application.Current == null)
            {
                var thread = new Thread(initializer.StartApplication);
                thread.SetApartmentState(ApartmentState.STA);

                lock (initializer)
                {
                    thread.Start();
                    Monitor.Wait(initializer);
                }
            }

            else if (Application.Current.Dispatcher.Thread == Thread.CurrentThread)
                initializer.Start();
            else
                Application.Current.Dispatcher.Invoke(
                    new StartupHandler(initializer.Start), new object[0]);

            return initializer.runtime;
        }

        private void StartApplication()
        {
            var application = new Application
            {
                ShutdownMode = ShutdownMode.OnExplicitShutdown
            };

            application.Startup += Start;
            application.Run();
        }

        private static string[] BootstrapPaths { get; set; }
    }
}
